/*
 * MIT License
 *
 * Copyright (c) 2020 wen.gu <454727014@qq.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/***************************************************************************
 * Name: stl_arg_parse.h
 *
 * Purpose: implementation argument parse for commandline parameter
 *
 * Developer:
 *   wen.gu , 2022-03-12
 *
 * TODO:
 *
 ***************************************************************************/

/******************************************************************************
 **    INCLUDES
 ******************************************************************************/
#ifndef __STL_ARG_PARSE_H__
#define __STL_ARG_PARSE_H__

#include <stdio.h>
#include <stdint.h>
#include <string.h>

/******************************************************************************
 **    MACROS
 ******************************************************************************/

#define STL_ARG_MANDATORY (1)
#define STL_ARG_OPTION    (0)

/** STL_ARG_PARAM_NONE: needn't parameter for a argument, but we need to known this argument is got,so set value "1" as got, default set "0" */
#define STL_ARG_PARAM_NONE (0) 
#define STL_ARG_PARAM_NEED (1)

/** auto add help useage info */
#define STL_ARG_USEAGE_ADD  (1)
#define STL_ARG_USEAGE_NONE (0)

#define STL_ARG_PREFIX ((char)'-')
#define STL_ARG_LONG_ARGUMENT_SEPARATE_CHAR '='
#define STL_ARG_NO_SHORT_NAME '\0'


#define STL_ARG_READER_INIT(reader, arg_cnt, arg_value) do{\
        (reader)->argc_ = arg_cnt; (reader)->argv_ = arg_value; (reader)->cur_idx_ = 0; \
    }while(0)

#define STL_ARG_READER_TOKEN(reader) ((reader)->cur_idx_ < (reader)->argc_) ? (reader)->argv_[(reader)->cur_idx_++] : NULL
#define STL_ARG_READER_CUR_IDX(reader) ((reader)->cur_idx_ > 0) ? ((reader)->cur_idx_ - 1) : 0

#define STL_ARG_OPTION_COUNT(option_list) (sizeof(option_list) / sizeof(stl_arg_option_t))

#define STL_ARG_VALUE_REG(value) ((const char**)&value)
/******************************************************************************
 **    TYPE DEFINITIONS
 ******************************************************************************/
#ifdef __cplusplus
extern "C"{
#endif

typedef enum _stl_arg_value_type
{
    STL_ARG_VALUE_INT = 0,
    STL_ARG_VALUE_FLOAT,
    STL_ARG_VALUE_STR,
}stl_arg_value_type_t;

typedef struct _stl_arg_option
{
    char short_name;              /** the short name of argument, e.g. h */
    const char* long_name;        /** the long name of argument, e.g. help */
    const char* description;      /** the descrtion of argument */
    int is_mandatory;             /** */
    int is_need_param;
    const char* default_value;
    stl_arg_value_type_t value_type;
    union stl_arg_parse
    {
        const char** value_str;      /** if set this, indicat receive the string of current value */   
        uint64_t* value_int;   /** if set this, indicat curent parameter must be integer */
        double* value_float;   /** if set this, indicat curent parameter must be double/float */
        
    }value;  
}stl_arg_option_t;

/** a helper struct to read argument*/
typedef struct _stl_arg_reader
{
    int cur_idx_;
    int argc_;
    char** argv_;    
}stl_arg_reader_t;
/******************************************************************************
 **    CLASSES/FUNCTIONS DEFINITIONS
 ******************************************************************************/


static void stl_arg_show_useage(const char* prog_name, stl_arg_option_t* option_list, int option_cnt, int auto_add_help_arg)
{
    printf("useage: %s ", prog_name);
    unsigned int max_name_len = 0;
    int has_help_info = 0;
    for (unsigned int i = 0; i < option_cnt; i++)
    {
        stl_arg_option_t* op = &option_list[i];
        if (op->is_mandatory)
        {
            printf("-%c %s ", op->short_name, op->is_need_param ? "xxx" : "");
        }

        if (op->long_name)
        {
            unsigned int len = strlen(op->long_name);
            if (len > max_name_len)
            {
                max_name_len = len;
            }
        }
    }

    max_name_len += 4; /** like add a 'tab' effect */

    printf("[options] ...\noptions:\n");
    for (unsigned int i = 0; i < option_cnt; i++)
    {
        stl_arg_option_t* op = &option_list[i];
        if (op->short_name != STL_ARG_NO_SHORT_NAME)
        {
            printf("  -%c, ", op->short_name);
        }
        else
        {
            printf("    , ");
        }

        if (op->long_name && (op->long_name[0] != '\0'))
        {
            printf("--%-*s", max_name_len, op->long_name);
        }
        else
        {
            printf("  %-*s", max_name_len, "");
        }

        if (op->description)
        {
            printf("%s.", op->description);
        }
        
        if (op->default_value)
        {
            printf(" [default = %s]", op->default_value);
        }
        printf("\n");
    }   

    if (auto_add_help_arg == STL_ARG_USEAGE_ADD)
    {
        printf("  -h, --%-*s%s\n", max_name_len, "help", "show this usage information");
    }    
}


static stl_arg_option_t* stl_arg_find_option_by_long_name(stl_arg_option_t* option_list, int option_cnt, const char* long_name)
{
    for (int i = 0; i < option_cnt; i++)
    {
        const char* op_long_name = option_list[i].long_name;
        if (op_long_name && (op_long_name[0] != '\0') && (strcmp(op_long_name, long_name) == 0))
        {
            return &option_list[i];
        }
    }

    return NULL;    
}

static stl_arg_option_t* stl_arg_find_option_by_short_name(stl_arg_option_t* option_list, int option_cnt, char short_name)
{
    for (int i = 0; i < option_cnt; i++)
    {
        if (option_list[i].short_name == short_name)
        {
            return &option_list[i];
        }
    }

    return NULL;    
}

static void stl_arg_option_set_value(stl_arg_option_t* option, const char* value_str)
{
    //printf("value: %s\n", value_str);
    switch (option->value_type)
    {
    case STL_ARG_VALUE_INT: 
    if (option->value.value_int)
    {
        sscanf(value_str, "%llu", option->value.value_int);
        //printf("got value: %llu\n", *option->value.value_int);
    }
     break;
    case STL_ARG_VALUE_FLOAT: 
    if (option->value.value_int)
    {
        sscanf(value_str, "%lf", option->value.value_float); 
    }
    break;
    case STL_ARG_VALUE_STR: 
    if (option->value.value_str)
    { 
        *option->value.value_str = value_str; 
    }
    break;
    default:
        break;
    }
}

static int stl_arg_parse_long_name_argument(stl_arg_option_t* option_list, int option_cnt, stl_arg_reader_t* reader,  char* cur_token)
{
    const char* separate_pos = strchr(cur_token, STL_ARG_LONG_ARGUMENT_SEPARATE_CHAR);
    stl_arg_option_t* op = NULL;
    int name_len = (separate_pos == NULL) ? strlen(cur_token) : (separate_pos - cur_token);

    cur_token[name_len] = '\0'; //todo check me?? 
    op = stl_arg_find_option_by_long_name(option_list, option_cnt, cur_token);

    if (!op)
    {
        printf("not found long argument: %s\n", cur_token);
        return -1;
    }

    if (op->is_need_param == STL_ARG_PARAM_NEED)
    {
        if (!separate_pos)
        {
            printf("syntax error at arg idx: %d, this argument need parameter, will be like: --long_name=xx \n", STL_ARG_READER_CUR_IDX(reader));
            return -1;              
        }

        stl_arg_option_set_value(op, separate_pos + 1); /** +1 to skip separate char '=' */

    }
    else
    {
        if (separate_pos)
        {
            printf("syntax error at arg idx: %d, this argument needn't parameter, will be like: --long_name \n", STL_ARG_READER_CUR_IDX(reader));
            return -1;                  
        }
        else
        {
            stl_arg_option_set_value(op, "1"); //todo refine me??  
        }
    }

    return 0;
}

static int stl_arg_parse_short_name_argument(stl_arg_option_t* option_list, int option_cnt, stl_arg_reader_t* reader, char short_name)
{
    stl_arg_option_t* op = op = stl_arg_find_option_by_short_name(option_list, option_cnt, short_name);
    if (!op)
    {
        printf("not found short argument: %c\n", short_name);
        return -1;        
    }

    if (op->is_need_param == STL_ARG_PARAM_NEED)
    {
        char* next_token = STL_ARG_READER_TOKEN(reader);

        if (!next_token || (next_token[0] == '\0') || (next_token[0] == STL_ARG_PREFIX))
        {
            printf("syntax error at arg idx: %d, this argument need parameter, will be like: -short_name xxx \n", STL_ARG_READER_CUR_IDX(reader));
            return -1;
        }

        stl_arg_option_set_value(op, next_token);
    }
    else
    {
        //printf("[%s.%d]===>:\n", __FUNCTION__, __LINE__);
        stl_arg_option_set_value(op, "1"); //todo refine me??
    }

    return 0; 
}

static int stl_arg_option_init(stl_arg_option_t* option_list, int option_cnt)
{
    for (unsigned int i = 0; i < option_cnt; i++)
    {
        stl_arg_option_t* op = &option_list[i];
        if (op->default_value)
        {/** first set default value to got value */
             stl_arg_option_set_value(op, op->default_value);
        }        

        /** todo something check other info */
    }
    
    return 0;
    
}

static int stl_arg_parse(int argc, char* argv[], int include_prog_name, stl_arg_option_t* option_list, int option_cnt, int auto_add_help_arg)
{
    if ((argc < 1) || (argv == NULL) || !option_list || (option_cnt == 0))
    {
        return -1;
    }
    if (stl_arg_option_init(option_list, option_cnt) != 0)
    {
        return -1;
    }
    stl_arg_reader_t arg_reader;
    STL_ARG_READER_INIT(&arg_reader, argc, argv);
    char* cur_token = STL_ARG_READER_TOKEN(&arg_reader);

    const char* prog_name = NULL;

    if (include_prog_name)
    {
        prog_name = cur_token;
        cur_token = STL_ARG_READER_TOKEN(&arg_reader);
    }

    if (cur_token == NULL)
    {
        stl_arg_show_useage(prog_name, option_list, option_cnt, auto_add_help_arg);
        return -1;
    }

    while (cur_token)
    {
        if (cur_token[0] == STL_ARG_PREFIX)
        {
            if (cur_token[1] == STL_ARG_PREFIX) /** mean that argument with long name: --xxx */
            {
                if (cur_token[2] == '\0') /** a long name argument at least 3 char: --x */
                {
                    printf("syntax error at arg idx: %d, a long name argument at least 3 char: --x \n", STL_ARG_READER_CUR_IDX(&arg_reader));
                    return -1;
                } 

                if ((auto_add_help_arg == STL_ARG_USEAGE_ADD) && (strcmp(cur_token + 2, "help") == 0)) /** +2 to skip "--" prefix */
                {
                    stl_arg_show_useage(prog_name, option_list, option_cnt, auto_add_help_arg);
                    return -2;
                }

                int ret = stl_arg_parse_long_name_argument(option_list, option_cnt, &arg_reader, cur_token + 2); /** + 2 to skip '--' */             
                if (ret != 0)
                {
                    return ret;
                }
            }
            else if (cur_token[1] == '\0')
            {
                printf("invalid argument: %s\n", cur_token);
                return -1; /** todo refine me */                   
            }          
            else /** this case mean that a short name argument */
            {
                if ((auto_add_help_arg == STL_ARG_USEAGE_ADD) && (cur_token[1] == 'h'))
                {
                    stl_arg_show_useage(prog_name, option_list, option_cnt, auto_add_help_arg);
                    return -2;
                }

                int ret = stl_arg_parse_short_name_argument(option_list, option_cnt, &arg_reader, *(cur_token + 1)); /** +1 to skip '-' */
                if (0 != ret)
                {
                    return ret;
                }                    
            }              
        }
        else
        {
            printf("invalid argument: %s\n", cur_token);
            return -1;
        }

        cur_token = STL_ARG_READER_TOKEN(&arg_reader);
    }

    return 0;
}


#ifdef __cplusplus
}
#endif

#endif /** !__STL_ARG_PARSE_H__ */

